package yipdf;

import java.awt.geom.AffineTransform;
import java.awt.image.CropImageFilter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

import com.itextpdf.text.Document;
import com.itextpdf.text.DocumentException;
import com.itextpdf.text.Rectangle;
import com.itextpdf.text.pdf.PdfContentByte;
import com.itextpdf.text.pdf.PdfImportedPage;
import com.itextpdf.text.pdf.PdfReader;
import com.itextpdf.text.pdf.PdfWriter;
import java.io.ByteArrayOutputStream;
import java.nio.charset.Charset;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Set;

class PDFiText implements IPDFProcessor {

	private IConsole console_;
	
	//private Rectangle userTrim_;

	PDFiText(IConsole console) {
		console_ = console;
	}

/*	public File process(File file, boolean landscape, String extractOption,
			String trimOption, boolean imposition4up, float scale,
			boolean cleanup) throws IOException {
		console_.print("Loading document from "+file.getName()+"...");
		byte[] input = readFile(file);
		byte[] output = null;
		File outputFile = getOutputFile(file);
		try {
			console_.print("Processing document...");
			PdfReader inputReader = new PdfReader(input);
			ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
			Rectangle pageSize = inputReader.getPageSize(1);
			Document document = new Document(pageSize);
			PdfWriter writer = PdfWriter.getInstance(document, outputStream);
			document.open();
			int maxPageNum = inputReader.getNumberOfPages();
			PdfContentByte cb = writer.getDirectContent();
			int pagesProcessed=0;
			for (Integer importPage : getUserSelection(maxPageNum, extractOption)) {
				int pos = pagesProcessed%4; // position in 4up layout: 0,1,2,3
				if (pos==0) document.newPage();
				double w = pageSize.getWidth();
				double h = pageSize.getHeight();
				AffineTransform transformer = new AffineTransform();
				if (landscape) {
					transformer.translate(w/2*(pos%2), h/2*(pos/2));
				}
				else {
					transformer.translate(w/2*(pos/2), h/2*(1-(pos%2)));
				}
				transformer.scale(0.5, 0.5);
				cb.addTemplate(writer.getImportedPage(inputReader, importPage), transformer);
				cb.stroke();
				pagesProcessed++;
			}
			document.close();
			output = outputStream.toByteArray();
			console_.print("Saving document to "+outputFile.getName()+"...");
			saveFile(outputFile, output);
			console_.print("Done!");
			return outputFile;
		}catch(DocumentException ex){
			ex.printStackTrace();
			throw new IOException("PDF processing error: "+ex.getMessage());
		}
	}*/

/*	public File process(File file, boolean landscape, String extractOption,
				String trimOption, boolean imposition4up, float scale,
				boolean cleanup) throws IOException {
			console_.print("Loading document from "+file.getName()+"...");
			byte[] input = readFile(file);
			byte[] output = null;
			File outputFile = getOutputFile(file);
			try {
				console_.print("Processing document...");
				PdfReader inputReader = new PdfReader(input);
				ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
				Rectangle pageSize = inputReader.getPageSize(1);
				Rectangle pageSizeRot = inputReader.getPageSizeWithRotation(1);
				Rectangle cropBox = getUserTrim(trimOption, inputReader.getCropBox(1));
				double w = pageSizeRot.getWidth();
				double h = pageSizeRot.getHeight();
				Document document = new Document(pageSize);
				PdfWriter writer = PdfWriter.getInstance(document, outputStream);
				document.open();
				int maxPageNum = inputReader.getNumberOfPages();
				PdfContentByte cb = writer.getDirectContent();
				int pagesProcessed=0;
				for (Integer importPage : getUserSelection(maxPageNum, extractOption)) {
					int pos = pagesProcessed%4; // position in 4up layout: 0,1,2,3
					if (pos==0) document.newPage();
					AffineTransform transformer = new AffineTransform();
					transformer.scale(0.5, 0.5);
					if (pageSizeRot.getRotation()==90) {
						transformer.rotate(-Math.PI/2);
						transformer.translate(-2*h,0);
						transformer.translate(h*(pos%2), w*(pos/2));
						//transformer.translate(2*(h-w),0);
					}
					else {
						transformer.translate(w*(pos/2), h*(1-(pos%2)));
					}
					PdfImportedPage tmpl = writer.getImportedPage(inputReader, importPage);
					tmpl.setBoundingBox(cropBox);
					cb.addTemplate(tmpl, transformer);
					cb.stroke();
					pagesProcessed++;
				}
				document.close();
				output = outputStream.toByteArray();
				console_.print("Saving document to "+outputFile.getName()+"...");
				saveFile(outputFile, output);
				console_.print("Done!");
				return outputFile;
			}catch(DocumentException ex){
				ex.printStackTrace();
				throw new IOException("PDF processing error: "+ex.getMessage());
			}
		}*/
	
	public File process(File file, boolean landscape, String extractOption,
			String trimOption, boolean imposition4up, float scale,
			boolean cleanup) throws IOException {
		console_.print("Loading PDF data from "+file.getName()+"...");
		byte[] data = readFile(file);
		File outputFile = getOutputFile(file);
		try {
			PdfReader inputReader = new PdfReader(data);
			Rectangle pageSizeRot = inputReader.getPageSizeWithRotation(1);
			Rectangle crop = inputReader.getCropBox(1);
			console_.print("Processing document...");
			if (extractOption!=null && extractOption.trim().length()>0) {
				data = processPagesSelection(data, extractOption);
			}

			if (imposition4up) {
				data = process4up(data, crop, trimOption);
			}
			else if (trimOption!=null && trimOption.trim().length()>0) {
			    //trimOption="0mm 0mm 0mm 0mm";
				data = processTrim(data, trimOption);
			}
			if (scale>0.0 && scale<1.0) {
				data = processScale(data, scale);
			}
			
			console_.print("Saving document to "+outputFile.getName()+"...");
			saveFile(outputFile, data);
			console_.print("Done!");
			return outputFile;
		}catch(DocumentException ex){
			ex.printStackTrace();
			throw new IOException("PDF processing error: "+ex.getMessage());
		}
	}
	
	private static File getOutputFile(File input) {
		String base = input.getName().substring(0, input.getName().length()-4);
		return new File(input.getParentFile(),base+"-transfo.pdf");
	}
	
    private static byte[] readFile(File file) throws FileNotFoundException, IOException{
        ByteArrayOutputStream output = new ByteArrayOutputStream();
        FileInputStream input = new FileInputStream(file);
        int inputbyte;
        while ((inputbyte = input.read())!=-1){
            output.write(inputbyte);
        }
        input.close();
        return output.toByteArray();
    }
    
    private static void saveFile(File file, byte[] bytes) throws FileNotFoundException, IOException{
        FileOutputStream output = new FileOutputStream(file);
        output.write(bytes);
        output.close();
    }
    
    private Rectangle getUserTrim(String trimSpec, Rectangle cropBox) throws IOException {
    	if (trimSpec==null || trimSpec.trim().length()==0) {
    		return cropBox;
    	}
    	String[] split = trimSpec.split(" +");
    	if (split.length!=4) {
    		throw new IOException("Invalid trim parameters");
    	}
    	float leftTrim = getValueInPoints(split[0]);
    	float bottomTrim = getValueInPoints(split[1]);
    	float rightTrim = getValueInPoints(split[2]);
    	float topTrim = getValueInPoints(split[3]);
    	Rectangle trim = new Rectangle(cropBox.getLeft()+leftTrim, cropBox.getBottom()+bottomTrim, cropBox.getRight()-rightTrim, cropBox.getTop()-topTrim);
    	return trim;
    }
    
    private float getValueInPoints(String value) throws IOException {
    	double factor=-1.0;
    	String inputValue=null;
    	if (value.endsWith("mm")) {
    		factor=0.3527272727272727;
    		inputValue=value.substring(0, value.length()-2);
    	}
    	else if (value.endsWith("cm")) {
    		factor=0.0352727272727273;
    		inputValue=value.substring(0, value.length()-2);
    	}
    	if (inputValue==null) {
    		throw new IOException("Invalid unit: "+value);
    	}
    	return (float) (Double.valueOf(inputValue)/factor);
    }
    
    private List<Integer> getUserSelection(int nbPages, String pagesSelection) throws IOException {
    	if (pagesSelection==null || pagesSelection.trim().length()==0) {
    		return new AllPagesList(nbPages);
    	}
    	ArrayList<Integer> pages = new ArrayList<Integer>();
    	try {
    		String[] sets = pagesSelection.split(",");
    		for (String set : sets) {
    			String[] indices = set.split("-",2);
    			if (indices.length==1) {
    				pages.add(Integer.valueOf(indices[0].trim()));
    			}
    			else {
    				int[] intBounds = getBoundsAsIntegers(indices, nbPages);
    				for (int i=intBounds[0]; i<=intBounds[1]; i++) {
    					pages.add(Integer.valueOf(i));
    				}
    			}
    		}
    		return pages;
    	}
    	catch(Exception e) {
    		throw new IOException("Syntax error in pages subset. "+e.getMessage());
    	}
    }
    

    
    private byte[] processPagesSelection(byte[] input, String pagesSelection) throws DocumentException, IOException {
		console_.print(" * Extracting pages subset: "+pagesSelection+"...");
		PdfReader inputReader = new PdfReader(input);
		ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
		Rectangle pageSize = inputReader.getPageSize(1);
		Document document = new Document(pageSize);
		PdfWriter writer = PdfWriter.getInstance(document, outputStream);
		document.open();
		int maxPageNum = inputReader.getNumberOfPages();
		PdfContentByte cb = writer.getDirectContent();
		for (Integer importPage : getUserSelection(maxPageNum, pagesSelection)) {
			document.newPage();
			cb.addTemplate(writer.getImportedPage(inputReader, importPage),new AffineTransform());
			cb.stroke();
		}
		document.close();
		return outputStream.toByteArray();
    }
    
    private byte[] processTrim(byte[] input, String trimOption) throws DocumentException, IOException {
		console_.print(" * Trimming: "+trimOption+"...");
		PdfReader inputReader = new PdfReader(input);
		ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
		Rectangle pageSize = inputReader.getPageSize(1);
		Document document = new Document(pageSize);
		PdfWriter writer = PdfWriter.getInstance(document, outputStream);
		Rectangle crop = getUserTrim(trimOption, inputReader.getCropBox(1));
		writer.setCropBoxSize(crop);
		document.open();
		int maxPageNum = inputReader.getNumberOfPages();
		PdfContentByte cb = writer.getDirectContent();
		for (int pageNum=1; pageNum<=maxPageNum; pageNum++) {
			document.newPage();
			AffineTransform transformer = new AffineTransform();
			cb.addTemplate(writer.getImportedPage(inputReader, pageNum), transformer);
			cb.stroke();
		}
		document.close();
		return outputStream.toByteArray();
    }
    
    private byte[] process4up(byte[] input, Rectangle docCrop, String trimOption)  throws DocumentException, IOException {
		console_.print(" * Refactoring document to 4up layout...");
		PdfReader inputReader = new PdfReader(input);
		ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
		Rectangle pageSize = inputReader.getPageSize(1);
		Rectangle pageSizeRot = inputReader.getPageSizeWithRotation(1);
		Rectangle crop = getUserTrim(trimOption, docCrop);
		Document document = new Document(pageSizeRot);
		PdfWriter writer = PdfWriter.getInstance(document, outputStream);
		document.open();
		int maxPageNum = inputReader.getNumberOfPages();
		PdfContentByte cb = writer.getDirectContent();
		boolean rot90 = pageSizeRot.getRotation()==90;
		for (int pageNum=1; pageNum<=maxPageNum; pageNum++) {
			int pos = (pageNum-1)%4; // position in 4up layout: 0,1,2,3
			if (pos==0) document.newPage();
			double w = pageSizeRot.getWidth();
			double h = pageSizeRot.getHeight();
			double wVisible=w, hVisible=h;
			AffineTransform transformer = new AffineTransform();
			if (crop!=null) {
				double sx = pageSize.getWidth()/crop.getWidth();
				double sy = pageSize.getHeight()/crop.getHeight();
				double dx = pageSize.getLeft()-crop.getLeft();
				double dy = pageSize.getBottom()-crop.getBottom();
				transformer.translate(rot90?dy:dx, rot90?dx:dy);
				transformer.scale(rot90?sy:sx, rot90?sx:sy);
				wVisible = rot90 ? crop.getHeight() : crop.getWidth();
				hVisible = rot90 ? crop.getWidth() : crop.getHeight();
			}
			transformer.scale(0.5, 0.5);
//			if (rot90) {
//				transformer.rotate(-Math.PI/2);
//				transformer.translate(-2*h,0);
//				transformer.translate(hVisible*(pos%2), wVisible*(pos/2));
//				//transformer.translate(2*(h-w),0);
//			}
//			else {
//				transformer.translate(wVisible*(pos/2), hVisible*(1-(pos%2)));
//			}
			PdfImportedPage tmpl = writer.getImportedPage(inputReader, pageNum);
			tmpl.setBoundingBox(crop);
			cb.addTemplate(tmpl, transformer);
			cb.stroke();
		}
		document.close();
		return outputStream.toByteArray();
    	
    }
    
//    private boolean areRectanglesEqual(Rectangle r1, Rectangle r2) {
//    	return r1.getLeft()==r2.getLeft() && r1.getBottom()==r2.getBottom()
//    	&& r1.getRight()==r2.getRight() && r1.getTop()==r2.getTop() && r1.getRotation()==r2.getRotation();
//    }
    
    private byte[] processScale(byte[] input, float scale) throws DocumentException, IOException {
		console_.print(" * Scaling pages, factor "+scale+"...");
		PdfReader inputReader = new PdfReader(input);
		ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
		Rectangle pageSize = inputReader.getPageSize(1);
		Document document = new Document(pageSize);
		PdfWriter writer = PdfWriter.getInstance(document, outputStream);
		document.open();
		int maxPageNum = inputReader.getNumberOfPages();
		PdfContentByte cb = writer.getDirectContent();
		for (int pageNum=1; pageNum<=maxPageNum; pageNum++) {
			document.newPage();
			AffineTransform transformer = new AffineTransform();
			transformer.translate(pageSize.getWidth()*(1.0-scale)/2, pageSize.getHeight()*(1.0-scale)/2);
			transformer.scale(scale, scale);
			cb.addTemplate(writer.getImportedPage(inputReader, pageNum), transformer);
			cb.stroke();
		}
		document.close();
		return outputStream.toByteArray();
    }
    private static int[] getBoundsAsIntegers(String[] bounds, int nbPages) {
    	int[] intBounds = new int[2];
    	String low=bounds[0].trim(), high=bounds[1].trim();
    	intBounds[0] = low.length()==0  ? 1       : Integer.valueOf(low);
    	intBounds[1] = high.length()==0 ? nbPages : Integer.valueOf(high);
    	return intBounds;
    }
    
    private static class AllPagesList extends AbstractList<Integer> {
    	
    	private int size_;
    	
    	AllPagesList(int size) {
    		size_ = size;
    	}
    	
		public Integer get(int index) {
			return Integer.valueOf(index)+1;
		}

		public int size() {
			return size_;
		}
    }

}
